"""
### 关于字符串数据类型的一点说明:
涉及到通信时,Python 3的字符串类型可能让人感到迷惑, 部分原因来自于Python 2
和Python 3的区别.这里简要介绍一下需要注意的地方, 具体原理可以问Bing娘.

总的来说,Python 3中只有一种保存文本信息的数据类型:str, 但同时还有一个看上
去非常相似的数据类型:bytes. 称呼上前者叫做"字符串",后者叫"字节序列". 字符
串保存的是Unicode文本, 而字节序列保存的是字节, 即取值范围在[0, 255] 的整数. 
首先, 两者在外观上非常相似, bytes的字面量(代码中直接给出的量)仅仅是在str的
字面量前加上前缀b:
```
>>> bb = b'abc'  # bytes 字面量
>>> ss =  'abc'  # str 字面量
```

其次, print()的结果也非常相似:
```
>>> print(bb)
b'abc'
>>> print(ss)
abc
```

再者, 二者都支持字符串格式化(但是只有str支持format方法):
```
>>> '%i, %.2f, %s' % (1, 2.2, 'aa')
'1, 2.20, aa'
>>> b'%i, %.2f, %s' % (1, 2.2, b'aa')
b'1, 2.20, aa'
```

总之, str 和 bytes 在行为上有很多共同点. 这是因为, Python 3 中的 bytes 演变自
Python 2 中的str, 而 Python 3 中的str, 则是一种新的数据类型. 

另外, str 和 bytes 都是不可变序列(immutable sequence). 至于具体什么是序列,
我至今尚未在书本中看到定义, 也许在官方doc里可以找到答案吧. 我根据自己所见
所闻的推断是, 在Python中, 序列是这样一种数据类型, 其中的元素是线性存储的, 
并且支持索引, 切片等操作. 典型且常用的序列有list, tuple, str 和 bytes. 

这么说的话, 可以把序列看作数据的"容器", 通过str和bytes是序列的本质, 我们可以
看出一些端倪. 我们先来看看二者作为"容器", 容纳的是什么元素:
```
>>> bb[0]
97
>>> ss[0]
'a'
>>> type(bb[0])
<class 'int'>
>>> type(ss[0])
<class 'str'>
```

可以看到, bytes中容纳的元素是int, 而str容纳的元素是只含单个字符的str(注意
Python 的数据类型中, 只有字符串, 没有字符). 接下来, 将两者转换为另一种序列
类型看看:
```
>>> list(bb)
[97, 98, 99]
>>> list(ss)
['a', 'b', 'c']
>>> print(bytes([97,98,99]))
b'abc'
```

可以看到, bytes转换成的list中,元素是整数, 而str转换成的list中, 元素是只含单个
字符的字符串. 

参考文献:
[1] Michal Jaworski.Python高级编程(第2版)[M]:P27-28

p.s.
### 我的一点个人见解.
从本质上来说, 数据(数值, 字符串, 音乐, 图片等)都是一串二进制
编码, 而这些编码究竟代表哪类数据则取决于我们如何去解释它(应用程序, 如字处理器, 
播放器). 这其中, 字符串是一类很重要的数据, 有专门的解释方案(ASCII, utf-8等编码),
以及相关的操作/算法(字符, 字符串的查找/匹配/替换等), 这些内容已有成熟的体系, 
显然为字符串提供一种数据类型会带来很大的方便. Python 中, 字符串类型 str 提供了
很多方法,如strip(), split(), upper(), replace(), join(), 使得字符串的处理变得
非常方便. 而对于一般的二进制数据(音乐, 图片等), 则以字节为单位保存在bytes中. 
Python 2中, 由于str的默认编码是ASCII, 天生就是以字节为单位保存的, 因此除了作为
字符串使用, 也可以代劳用于保存一般二进制数据. 但是这看起来有点别扭, 本来引入str
类型的目的就是用来保存字符串, 现在却用来保存一般二进制代码, 逻辑上不够清晰, 容
易产生迷惑. 另一方面, 默认编码采用ASCII, 在显示非英语文字时, 很容易遇到问题.
所以 Python 3 做出了两个改变, 第一是引入新的类型bytes, 保存一般二进制数据的任务
就交给它了, 第二是默认编码采用Unicode, 这样可以减少显示多语言文字的问题. 以上, 
随手码出, 未作求证.


### bytes 与 str 之间的相互转换
- 通过encode和decode方法
很容易有这种感觉: bytes是一般二进制数据, 而str则是专门的应用型数据, 因此可以这样
来记忆二者之间的转换: 将 str 编码成二进制(bytes)用encode, 而从二进制解码得到
str 则用decode.
```
>>> sb = bb.decode()
>>> type(sb)
<class 'str'>
>>> bs = ss.encode()
>>> type(bs)
<class 'bytes'>
```

### bytes使用索引的陷阱
对于df=b"<abc>", 你也许期望通过索引, 即df[0]来得到b'<', 就像字符串类型一样. 然
而事实却和你预想的不一样:
```
>>> df=b"<abc>"
>>> df[0]
60
```

通过索引, 你取出了字节序列的一个元素, 是一个整数. 如果要达到你的目的, 就应该使用
切片而不是索引:
```
>>> df[:1]
b'<'
```

### 那它们跟这份代码有什么关系呢
在底层硬件的通信中, 数据都是以二进制的形式传输的, 因此在这些通信的接口中, 使用
bytes作为输入输出的数据类型是非常自然的. 例如pyserial模块中, Serial.read()和
Serial.write() 传入/传出的参数都是bytes类型. 由于我们只用到ASCII码, 因此涉及到
通信的所有代码均使用bytes类型. 唯一的例外是 DataFrame 对象, 由于应用层代码主要
使用 str 类型, 因此DataFrame对象的label属性为str类型.

@author Stone at BlueNet, GDOU
@date Oct. 2020
"""

from collections import namedtuple


SYM_START = b'<'  # 帧开始字符
SYM_END   = b'>'  # 帧结束字符
SEP_LABEL = b':'  # 标签分隔字符
SEP_VALUE = b','  # 数值分隔字符

# ENCODING = 'ascii' # 字符串与bytes相互转换所使用的编码


# ===== ===== ===== ===== =====
# Data Structures
# ===== ===== ===== ===== =====
DataFrame = namedtuple('DataFrame', ['label','values'], defaults=['', []])


# ===== ===== ===== ===== =====
# Classes
# ===== ===== ===== ===== =====
class Communication(object):
    def __init__(self, transceiver):
        """
        Requested interface for ``transceiver``:
            file-like interface: 
                read(size:int) -> bytes(),
                write(bytes():data),
            All those interfaces accept and return data with bytes object.
        """
        self.transceiver = transceiver
        self.reader = Reader()
        self.writer = Writer()
    
    def read_data(self):
        """尝试读取一个数据帧,若成功则返回一个DataFrame对象,否则返回None."""
        # ============== old code =============================================
        # c = self.transceiver.read()     # fetch a character
        # if self.reader.collect(c):      # wait for a complete data frame string
            # return parse(self.reader.buffer)
        # return None
        # ============== old code end =========================================
        if self.reader.collect(self.transceiver):
            return parse(self.reader.buffer)
        return None

    def pack_data(self, data_frame):
        return self.writer.pack_data(data_frame)
    
    def write_data(self, label, values=None):
        if values is None:
            data = DataFrame(label)
        else:
            data = DataFrame(label, values)
        
        data_str = self.pack_data(data)
        self.transceiver.write(data_str)


class Reader(object):
    def __init__(self):
        self.buffer = bytearray()       # 接收缓存
        self.is_start_detected = False  # 是否接收到SYM_START
    
    def __collect_with_bug(self, ch):
        """
        因为使用过程中发现该函数有bug，因此就在后面加上with_bug以作提醒，
        并使用双下划线前缀尽力隐藏起来。
        TODO 研究一下出错原因，以及怎么修复这个函数
        以下是原注释：
        ----------------------------------------------------------------
        
        检测是否接收到了完整的数据帧字符串.
        :param ch: 接收到的字符.
        :returns: 如果接收到完整的帧字符串则返回``True``,否则返回``False``.
        """
        
        self.buffer.extend(ch)
        
        if ch == SYM_END and self.is_start_detected:
            self.is_start_detected = False  # reset status
            
            return True  # 接收到完整的数据帧
        
        elif ch == SYM_START:
            self.is_start_detected = True
            self.buffer.clear()    # reset buffer
            self.buffer.extend(ch)
            
        return False

    def collect(self, transceiver):
        """由于原来的collect函数出了bug，时常丢失数据，而且是具有一定的概率偶然发生的，
        暂时还没有想到办法复现。但使用pyserial提供的readline函数读取串口则非常稳定，
        从未发生丢失的情况，因此暂时不考虑I2C等通信，利用pyserial提供的read_until()函数
        来检测数据帧字符串的首尾完整性。
        
        :param transceiver: pyserial的Serial对象.
        :returns: 如果接收到完整的帧字符串则返回``True``,否则返回``False``.
        """
        
        ret = False
        
        if not self.is_start_detected:
            # 等候帧开始字符
            recv = transceiver.read_until(SYM_START)
            
            # 因为串口读超时的原因，可能未收到完整的数据，因此检测是否确实收到
            # 想要的字符
            if recv[-1:] == SYM_START:
                self.is_start_detected = True
                
                self.buffer.clear()
                self.buffer.extend(recv[-1:])
        
        if self.is_start_detected:
            recv = transceiver.read_until(SYM_END)
            
            if recv[-1:] == SYM_END:
                self.is_start_detected = False
                ret = True
                
                self.buffer.extend(recv)
        
        return ret
    
    def collect2(self, transceiver):
        """发现新collect居然还有同样的bug，尝试以另一种方式实现"""
        recv = transceiver.readline().strip()
        if SYM_START in recv and SYM_END in recv:
            self.buffer.clear()
            self.buffer.extend(recv)
            return True
        return False
        

class Writer(object):
    def pack_data(self, df):
        """将数据帧对象转换成符合格式要求的数据帧字符串"""
        
        val_str = [str(x).encode() for x in df.values]
        df_str = b'%s%s%s%s%s' % (
            SYM_START, df.label.encode(), SEP_LABEL, SEP_VALUE.join(val_str), SYM_END)
        
        return df_str


def parse(df_str):
    """解析数据帧字符串.默认传入的数据帧字符串是合法的.也就是说,对于缺少帧开始/结束
    字符的数据帧字符串来说,仍然能正常解析,例如"tomas:44,55>"(缺帧开始字符),
    "<Tomas:44,55"(缺帧结束字符).因此数据帧字符串的完整性应由接受步骤保证.
    
    :param df_str <str>: 帧缓存字符串
    :returns: DataFrame对象
    """
    print(df_str, end="|")  # debug
    data = DataFrame()
    
    df_str = df_str.strip(b'%s%s ' %(SYM_START, SYM_END))  # 去除字符串首尾的帧开始/结束字符以及空格
    df_str = df_str.split(SEP_LABEL)  # 分裂标签子字符串和其余字符串
    label = df_str[0].decode()
    
    if len(df_str) == 2 and df_str[1].strip():  # 数据帧字符串中包含值的情况
        values = df_str[1].split(SEP_VALUE)  # 分裂数值
        values = [float(x) for x in values]
        
        return DataFrame(label=label, values=values)
    
    else:  # 数据帧字符串中不包含值,即 <lb> <lb:> 的情况
        return DataFrame(label=label)



# ===== ===== ===== ===== =====
# Test
# ===== ===== ===== ===== =====
class FakeUASRT(object):
    def __init__(self, bytes_stream):
        self.str_in = bytes_stream
    
        self.cur = 0
        self.length = len(self.str_in)
        self.src = self.gen_char()
    
    def gen_char(self):
        for c in self.str_in:
            yield c
    
    
    def read(self, size=1):
        out = bytearray()
        
        for i in range(size):
            c = next(self.src)
            out.append(c)
        
        return bytes(out)


def test_fake_uasrt(ser):
    for i in range(100):
        print(ser.read(i))


def test_reader(ser):
    reader = Reader()
    
    while True:
        c = ser.read()
        if reader.collect(c):
            print(reader.buffer)


def test_parse(df_strings):
    for s in df_strings:
        try:
            print(parse(s))
        except ValueError:
            print(b"error when parsing: %s" %s)


def test_read_data(ser):
    comm = Communication(ser)
    
    while True:
        data = comm.read_data()
        if data is not None:
            print(data)


def test_pack_data():
    writer = Writer()
    
    data = DataFrame(label=b"tomas", values=[6,66,666])
    df_str = writer.pack_data(data)
    print(df_str)


if __name__ == '__main__':
    strings = [
        # valid to parse
        b"<tomas: 44, 55>",
        b" < tt > ",
        b" < xx:> ",
        b" tomas: 44, 55> ",
        b" <tomas: 44, 55 ",
        b" tomas: 44, 55 ",
        
        # invalid to parse
        b'<tomas 44, 55>',
        b'<tomas: 44 55>'
    ]
    
    string_stream = """
    # valid data frame string
    <tomas: 44, 55>
    < tt >
    < xx:>
    
    # invalid data frame string
    tomas: 44, 55>
    """.encode()
    
    fake_uasrt = FakeUASRT(string_stream)
    
    
    test_parse(strings)
    # test_pack_data()

    
    try:
        # test_fake_uasrt(fake_uasrt)
        # test_reader(fake_uasrt)
        # test_read_data(fake_uasrt)
        pass
    except StopIteration:
        print()
        print("Stop Iteration\n\n")
    
    
    